//
//  Data
//
//  Created by Caue W. on 2011-03-28.
//  Copyright (c) 2011 __MyCompanyName__. All rights reserved.
//
package time.compiler.types;
import time.Errors;
using time.compiler.types.Data;

enum DBType
{
	DBBasic(b:BasicType);
	DBToMany(t:DBType);
	DBInst(t:InstInfo);
	DBEnum(t:EnumInfo);
	//DBAnon(t:AnonInfo); //TODO implement anonymous types
	DBLazy(ref:{ref:DBType});
}

enum DBModule
{
	DBClassDecl(i:InstInfo);
	DBEnumDecl(e:EnumInfo);
}

class DBTypeExtension
{
	public static function toString(me:DBType) : String
	{
		return switch(me)
		{
			case DBBasic(b):
				switch(b)
				{
					case BKey(k): "Key<" + me.toString() + ">";
					case BVarchar(maxSize): "Varchar<" + maxSize + ">";
					case BText: "Text";
					case BDate: "Date";
					case BInt: "Int";
					case BInt32: "haxe.Int32";
					case BInt64: "haxe.Int64";
					case BBool: "Bool";
					case BDouble: "Float";
					case BBigInt: "time.types.complex.BigInt";
					case BDecimal: "time.types.complex.Decimal";
					case BVarBlob(size): "Varblob<" + size + ">";
					case BBlob: "haxe.io.Bytes";
					case BVoid: "Void";
					case BFunction: "Function";
					case BSerializedType: "SerializedType";
					
				}
			case DBInst(t): t.path;
			case DBEnum(t): t.path;
			//case DBAnon(t): "Anon {}"; //TODO implement anonymous type toString
			case DBToMany(t): "ToMany<" + toString(t) + ">";
			case DBLazy(r): if (r == null || r.ref == null) "<Ref not filled>"; else toString(r.ref);
		}
	}
	
	public static function isBasic(t:DBType) : Bool
	{
		return switch(t.follow())
		{
			case BBasic(_):
				true;
			default:
				false;
		}
	}
	
	
	public static function toNormalizedString(me:DBType) : String
	{
		var t = me.toString();
		var sb = new StringBuf();
		for (i in 0...t.length)
		{
			var cur = t.charCodeAt(i);
			switch(cur)
			{
				case "<".code, ">".code, ".".code, "(".code, ")".code, " ".code: cur = "_".code;
			}
			sb.addChar(cur);
		}
		return sb.toString();
	}
	
	
	public static function compareType(m1:Member, m2:Member) : Bool
	{
		return m1.type.equals(m2.type) && Type.enumEq(m1.kind, m2.kind);
	}
	
	
	public static function equals(me:DBType, other:DBType):Bool
	{
		//TODO REALLY change this -- just a function to get something working
		trace("i need to be changed");
		
		return toString(follow(me)) == toString(follow(other));
	}
	
	public static function follow(t:DBType):Null<DBType>
	{
		return switch(t)
		{
			case DBLazy(f):
				if (f.ref == null)
					null;
				else
					follow(f.ref);
			default:
				t;
		}
	}
	
	public static function setRef(t:Null<DBType>, to:DBType, ?fst:Null<DBType>):DBType
	{
		if (null == t)
			return t;
		
		return switch(t)
		{
			case DBLazy(r):
				if (null == r.ref)
				{
					r.ref = to;
					if (null != fst)
						fst;
					else
						t;
				} else {
					setRef(r.ref, to, (null != fst) ? fst : t);
				}
			default: throw "DBLazy expected. Got " + t.toString();
		}
	}
}

typedef DBCommon =
{
	type:HierarchyType
}

typedef DBInfo =
{
	>DBCommon,
	
	//path used by database
	dbPath:String,
	//true underlying type's path
	path:String,
}

typedef InstInfo =
{
	>DBInfo,
	
	idField:String, //field name of the id, if any
	orderedMembers:Array<Member>,
	members:Hash<Member>,
	superClass:Null<InstInfo>,
	isWrapper:Null<DBType>, //there are some temporary classes that can be created when used Wrap<> on illegal classes. Those classes will have this parameter set to the class they reference
	indices:Array<IndexInfo>,
	//TODO - but not here; should be on a later phase
	//directSubclasses:Array<InstInfo>
}

enum IndexInfo
{
	IPoint(field:Array<String>); //field is an array of string since it can have more than one depth (for Value TypeBuilder)
	IRange(field:Array<String>, asc:Bool); //range = field range
	IMulti(indices:Array<{field:Array<String>, asc:Null<Bool>}>); //asc = null -> IPoint
}



typedef EnumInfo =
{
	>DBInfo,
	
	//types:DBTypeContainer<Array<EnumTypeInfo>>,
	members:Hash<Array<Member>>,
	ctors:Array<String>,
	/**
	 * Contains all enum field names, regardless of the constructor. 
	 */
	fields:Hash<{ctor:String, idx:Int, member:Member}>,
	builtMembers:Array<Member>
}

typedef EnumTypeInfo = 
{
	type:DBType,
	count:Int
}

typedef AnonInfo =
{
	>DBCommon,
	
	members:Hash<Member>
}

//HUniqueChild > HRoot > HUnknown
enum HierarchyType
{
	HUnknown; //Still not determined; All data in this state will be read as Root on the last pass
	HValueType; //Embeddable. Cannot be Ref<> type.
	HUniqueChild(p:Null<{type:DBType, parentField:String, valueType:Bool}>); //If the child only has one parent associated with it, it can be referenced dynamically
	HChild(parents:Array<{type:DBType, parentField:String, valueType:Bool}>); //No Ref<> type is possible (ie it exists on two different places)
	HRoot; //it's its own root
}

//TODO
/*enum DefaultValue
{
	DNow;
	DString(s:String);
	DInt(i:Int);
	DFloat(f:Float);
	DInt64(long:haxe.Int64);
	DBigInt(v:String);
	DDecimal(v:String);
}*/

typedef Member = 
{
	kind:MemberKind,
	type:DBType,
	name:String,
	dbName:String,
	//TODO defaults:DefaultValue
}

/*
	how will the objectstore handle this type? ValueType will be unwrapped; Basic will decode the data appropriately;
	Serialized will call serialize/unserialize AND check the created type
*/
enum MemberKind
{
	MValueType; //cannot accept ToMany
	//TODO how to deal with Array<{something:String, other:Int}> ? . Maybe create a temporary class ?
	MRelation(relationType:RelationType); //relations can only be established to a class
	MReference(to:DBType, refOptions:RefOptions);
	MBackReference(referencedType:DBType, referencedField:String, refOptions:Null<RefOptions>);
	MSerialized;
}

typedef RefOptions = { isToMany:Bool, hasBackReference:Null<Bool>, isBackReferenceToMany:Bool };

enum RelationType
{
	ParentToChild;
	ChildToParent(parentField:Null<String>);
	ChildrenToParent(parentField:Null<String>);
	ParentToChildren;
}

enum BasicType
{
	BKey(to:DBType);
	BVarchar(maxSize:Int); //TODO change me
	BText;
	BDate;
	BInt;
	BInt32;
	BInt64;
	BBool;
	BDouble;
	BBigInt;
	BDecimal;
	BVarBlob(size:Int);
	BBlob;
	BVoid;
	BFunction;
	BSerializedType;
}

class DBTypeContainer<T>
{
	var s:Hash<T>;
	var defaultT:Null<T>;
	
	public function new(?defaultT:Null<T>)
	{
		this.s = new Hash();
		this.defaultT = defaultT;
	}
	
	public function add(t:DBType, s:T)
	{
		var str = dbTypeToToken(t);
		this.s.set(str, s);
	}
	
	public function getAll(type:DBType) : Array<T>
	{
		var ret = [];
		var isToMany = false;
		while(type != null)
		{
			var t = type.follow();
			type = null;
			switch(t)
			{
				case DBInst(c):
					var token = (isToMany) ? dbTypeToToken(DBToMany(t)) : dbTypeToToken(t);
					if (this.s.exists(token))
					{
						ret.push(this.s.get(token));
					}
					
					var s = c.superClass;
					if (s != null)
					{
						type = DBInst(s);
					} else {
						type = null;
						
						if (isToMany)
						{
							if (this.s.exists("DBToMany<*>"))
								ret.push(this.s.get("DBToMany<*>"));
						} else {
							if (this.s.exists("DBInst(*)"))
								ret.push(this.s.get("DBInst(*)"));
						}
					}
					
				case DBToMany(t):
					if (isToMany) //TODO support DBToMany<DBToMany<>>
					{
						throw "Invalid DBToMany<DBToMany<>> configuration";
					}
					
					isToMany = true;
					type = t;
				case DBEnum(_), DBBasic(_):
					var token = (isToMany) ? dbTypeToToken(DBToMany(t)) : dbTypeToToken(t);
					if (this.s.exists(token))
					{
						ret.push(this.s.get(token));
					}
					
					var str = Type.enumConstructor(t) + "(*)";
					if (this.s.exists(str))
						ret.push(this.s.get(str));
					
				case DBLazy(_): throw Assert;
			}
		}
		
		if (defaultT != null)
			ret.push(defaultT);
			
		return ret;
	}
	
	public function get(type:DBType) : Null<T>
	{
		var isToMany = false;
		while(type != null)
		{
			var t = type.follow();
			type = null;
			switch(t)
			{
				case DBInst(c):
					var token = (isToMany) ? dbTypeToToken(DBToMany(t)) : dbTypeToToken(t);
					if (this.s.exists(token))
					{
						return this.s.get(token);
					} else {
						var s = c.superClass;
						if (s != null)
						{
							type = DBInst(s);
						} else {
							type = null;
							
							if (isToMany)
							{
								if (this.s.exists("DBToMany<*>"))
									return this.s.get("DBToMany<*>");
							} else {
								if (this.s.exists("DBInst(*)"))
									return this.s.get("DBInst(*)");
							}
						}
					}
				case DBToMany(t):
					if (isToMany) //TODO support DBToMany<DBToMany<>>
					{
						throw "Invalid DBToMany<DBToMany<>> configuration";
					}
					
					isToMany = true;
					type = t;
				case DBEnum(_), DBBasic(_):
					var token = (isToMany) ? dbTypeToToken(DBToMany(t)) : dbTypeToToken(t);
					if (this.s.exists(token))
					{
						return this.s.get(token);
					} else {
						var str = Type.enumConstructor(t) + "(*)";
						if (this.s.exists(str))
							return this.s.get(str);
					}
				case DBLazy(_): throw "assert";
			}
		}
		
		return defaultT;
	}
	
	
	
	function dbTypeToToken(t:DBType)
	{
		switch(t)
		{
			case DBBasic(b):
				return "DBBasic(" + Type.enumConstructor(b) + ")";
			//case DBAnon(a): //TODO support Anonymous types
			//	return null;
			case DBInst(c):
				return "DBInst(" + c.path + ")";
			case DBEnum(e):
				return "DBEnum(" + e.path + ")";
			case DBToMany(t):
				return "DBToMany<" + dbTypeToToken(t) + ">";
			case DBLazy(l):
				return dbTypeToToken(l.ref);
		}
	}
}